package de.stefanocke.roo.wicket.persistence;

import static de.stefanocke.roo.xtend.MemberTypeAdditionsHelper.createMemberTypeAdditions;

import java.util.ArrayList;
import java.util.List;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.springframework.roo.classpath.details.MethodMetadata;
import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem;
import org.springframework.roo.classpath.layers.MemberTypeAdditions;
import org.springframework.roo.classpath.layers.MethodParameter;
import org.springframework.roo.classpath.scanner.MemberDetails;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.model.SpringJavaType;

import de.stefanocke.roo.wicket.MoreMemberFindingUtils;
import de.stefanocke.roo.wicket.RooPersistenceBinding;
import de.stefanocke.roo.wicket.Stereotypes;
import de.stefanocke.roo.wicket.base.ModelClassAwareItdMetadataProviderBase;
import de.stefanocke.roo.wicket.base.ModelClassAwareMetadataCreationParams;
import de.stefanocke.roo.wicket.entity.EntityInterfacesMetadata;
import de.stefanocke.roo.wicket.model.JavaTypeMetadataDetails;
import de.stefanocke.roo.wicket.model.ModelClassService;
import de.stefanocke.roo.xtend.Parameter;

/**
 * Provides metadata for Roo persistence service binding.
 * 
 */
@Component
@Service
public final class PersistenceBindingMetadataProvider extends
		ModelClassAwareItdMetadataProviderBase<PersistenceBindingMetadata, PersistenceBindingAnnotationValues> {

	@Reference
	protected PersistenceMetadataService persistenceMetadataService;
	
	private static final JavaType AUTOWIRED = SpringJavaType.AUTOWIRED;

	public PersistenceBindingMetadataProvider() {
		super(RooPersistenceBinding.class, PersistenceBindingMetadata.class, PersistenceBindingAnnotationValues.class);
	}

	@Override
	protected ItdTypeDetailsProvidingMetadataItem getMetadataInternal(ModelClassAwareMetadataCreationParams<PersistenceBindingAnnotationValues> cmcp) {
		MemberDetails memberDetails = modelClassService.getMemberDetails(cmcp.getModelClass(), cmcp.getMid());
		PersistenceMetadataDetails persistenceDetails = getPersistenceDetails(cmcp.getModelClass(),
				memberDetails, cmcp.getMid());
		MemberTypeAdditions queryMetadata = getQueryMetadata(cmcp, persistenceDetails, memberDetails);
		MemberTypeAdditions countQueryMetadata = getCountQueryMetadata(cmcp, persistenceDetails, memberDetails);

		return new PersistenceBindingMetadata(cmcp, persistenceDetails, queryMetadata, countQueryMetadata,
				getCustomQueryParams(cmcp, queryMetadata));
	}

	private PersistenceMetadataDetails getPersistenceDetails(JavaType modelClass, MemberDetails memberDetails,
			String mid) {
		PersistenceMetadataDetails details = persistenceMetadataService.getJavaTypePersistenceMetadataDetails(
				modelClass, memberDetails, mid);
		if (details == null) {
			validationError(mid, modelClass + " is not a persistent type.");
		}
		return details;
	}

	private List<Parameter> getCustomQueryParams(
			ModelClassAwareMetadataCreationParams<PersistenceBindingAnnotationValues> cmcp,
			MemberTypeAdditions queryMetadata) {
		if (!isCustomQuery(cmcp)) {
			return new ArrayList<Parameter>();
		}
		List<Parameter> queryParams = new ArrayList<Parameter>();

		for (MethodParameter p : queryMetadata.getMethodParameters()) {
			queryParams.add(new Parameter(null, p.getKey(), p.getValue()));
		}
		return queryParams;

	}

	private boolean isCustomQuery(ModelClassAwareMetadataCreationParams<PersistenceBindingAnnotationValues> cmcp) {
		return cmcp.getAnnotationValues().queryName != null;
	}

	private MemberTypeAdditions getQueryMetadata(
			ModelClassAwareMetadataCreationParams<PersistenceBindingAnnotationValues> cmcp,
			PersistenceMetadataDetails persistenceDetails, MemberDetails memberDetails) {
		PersistenceBindingAnnotationValues av = cmcp.getAnnotationValues();
		if (av.queryName == null) {
			MemberTypeAdditions method = persistenceDetails.getFindEntriesMethod();
			if (method == null) {
				validationError(cmcp.getMid(), "No findEntries() method found for modelClass " + cmcp.getModelClass());
			}
			return method;
		} else {
			MethodMetadata methodMetadata = getRequiredMethod(cmcp, memberDetails, av.queryName);
			validateMethodReturnType(cmcp.getMid(), methodMetadata,
					javaType("javax.persistence.TypedQuery", cmcp.getModelClass()));
			// TODO: Allow service methods to be invoked.
			return createMemberTypeAdditions(cmcp.getMid(), cmcp.getModelClass(), methodMetadata, AUTOWIRED);
		}
	}

	private MemberTypeAdditions getCountQueryMetadata(
			ModelClassAwareMetadataCreationParams<PersistenceBindingAnnotationValues> cmcp,
			PersistenceMetadataDetails persistenceDetails, MemberDetails memberDetails) {
		PersistenceBindingAnnotationValues av = cmcp.getAnnotationValues();

		if (av.countQueryName == null) {
			if (av.queryName != null) {
				return null; // TODO: Try to find the count query by a naming
								// convention?
			} else {
				MemberTypeAdditions method = persistenceDetails.getCountMethod();
				if (method == null) {
					validationError(cmcp.getMid(), "No count() method found for modelClass " + cmcp.getModelClass());
				}
				return method;
			}
		} else {
			if (av.queryName == null) {
				validationError(cmcp.getMid(),
						"It is not allowed to specify a custom countQueryName without a custom queryName");
			}
			MethodMetadata methodMetadata = getRequiredMethod(cmcp, memberDetails, av.countQueryName);
			validateMethodReturnType(cmcp.getMid(), methodMetadata, JavaType.LONG_OBJECT, JavaType.LONG_PRIMITIVE,
					JavaType.INT_OBJECT, JavaType.INT_PRIMITIVE);

			// TODO: Allow service methods to be invoked.
			return createMemberTypeAdditions(cmcp.getMid(), cmcp.getModelClass(), methodMetadata, AUTOWIRED);
		}
	}

	private MethodMetadata getRequiredMethod(
			ModelClassAwareMetadataCreationParams<PersistenceBindingAnnotationValues> cmcp, MemberDetails memberDetails, String name) {
		MethodMetadata method = MoreMemberFindingUtils.getMethod(memberDetails,
				new JavaSymbolName(name));
		if (method == null) {
			validationError(cmcp.getMid(), "Method " + name + " not found in modelClass " + cmcp.getModelClass() + ".");
		}
		return method;
	}

	@Override
	protected String[] optionalStereotypes() {
		return EntityInterfacesMetadata.INTERFACES;
	}

	@Override
	protected String requiredStereotype() {
		return Stereotypes.PERSISTENCE_BINDINGS;
	}

}